package penguin.masking;

import penguin.masking.asm.MaskingObjectMethodVisitor;
import penguin.masking.asm.MaskingStringMethodVisitor;
import penguin.masking.asm.ReductionMethodVisitor;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author wuxh
 * @version 1.0.0
 */
public class MaskingContext implements RecyclableResource {

    public static final int STATE_SLEEP = -1, STATE_RUNNING = 1, STATE_STOPPED = 2;

    static volatile MaskingContext context = null;
    private Interceptors interceptors;
    private DataMaskingProcess maskingProcess;
    private int state = STATE_SLEEP;//-1：未启动，1：运行中，2：停止
    private List<RecyclableResource> resources = new ArrayList<>();
    private WordManager wordManager;
    private ContextConfigLoad configLoad;

    public static MaskingContext context() {
        if (context == null) {
            synchronized (MaskingContext.class) {
                if (context == null) {
                    context = new MaskingContext();
                }
            }
        }
        return context;
    }

    public void start() {
        iniContext();
        this.state = STATE_RUNNING;
    }

    public boolean uninstall() {
        this.state = STATE_SLEEP;
        interceptors.setRestore(true);
        free();
        context = null;
        return true;
    }

    public void stop() {
        this.state = STATE_STOPPED;
        interceptors.setRestore(true);
    }

    public boolean isStopped() {
        return state == STATE_STOPPED;
    }

    public boolean isRunning() {
        return state == STATE_RUNNING;
    }

    public boolean isUninstall() {
        return state == STATE_SLEEP;
    }

    public void addResource(RecyclableResource resource) {
        resources.add(resource);
    }

    private void iniContext() {
        loadProperties();
        Logger.level = getLogLevel();
        Logger.info("properties:" + configLoad.getProperties().toString());
        Logger.debug("Masking Context ini start");
        loadAgentConfig();
        CACHE_LIST = read ? new DefaultCache() : ServiceProvider.reductionCache();
        id = new AtomicLong(CACHE_LIST.initId());
        wordManager = ServiceProvider.wordManager();
        if (wordManager == null) {
            wordManager = new FileWordManager();
        }
        wordManager.setReloadTimes(refreshTime);
        wordManager.refreshWords();

        maskingProcess = new DataMaskingProcessImpl();
        maskingProcess.init(wordManager);
        addResource(CACHE_LIST);
        addResource(wordManager);
        Logger.debug("Masking Context ini end");
    }

    private void loadAgentConfig() {
        interceptors = new Interceptors();
        MaskingReflectionScan scan = new MaskingReflectionScan(getDriverScan());
        List<String> connections = scan.connections();
        for (String connection : connections) {
            //"(Ljava/lang/String;)Ljava/sql/PreparedStatement;" --> java.sql.PreparedStatement prepareStatement(String sql)
            interceptors.add(
                    new InterceptorConfig(connection, "prepareStatement", "(Ljava/lang/String;)Ljava/sql/PreparedStatement;",
                            ReductionMethodVisitor.class));
        }
        List<String> preparedStatements = scan.preparedStatements();
        for (String preparedStatement : preparedStatements) {
            //"(Ljava/lang/String;)Ljava/sql/PreparedStatement;" --> java.sql.PreparedStatement prepareStatement(String sql)
            interceptors.add(new InterceptorConfig(preparedStatement, "setString", "(ILjava/lang/String;)V", ReductionMethodVisitor.class));
            //"(Ljava/lang/Object;)Ljava/sql/PreparedStatement;" --> java.sql.PreparedStatement setObject(int parameterIndex, Object x)
            interceptors.add(new InterceptorConfig(preparedStatement, "setObject", "(ILjava/lang/Object;)V", ReductionMethodVisitor.class));
        }
        List<String> statements = scan.statements();
        for (String statement : statements) {
            //"(Ljava/lang/String;)Z" --> public boolean execute(String sql)
            interceptors.add(new InterceptorConfig(statement, "execute", "(Ljava/lang/String;)Z", ReductionMethodVisitor.class));
        }
        List<String> resultSetImpls = scan.resultSets();
        for (String resultSetImpl : resultSetImpls) {
            //"(I)Ljava/lang/String;" --> String getString(int columnIndex)
            interceptors.add(new InterceptorConfig(resultSetImpl, "getString", "(I)Ljava/lang/String;", MaskingStringMethodVisitor.class));
            interceptors.add(new InterceptorConfig(resultSetImpl, "getObject", "(I)Ljava/lang/Object;", MaskingObjectMethodVisitor.class));
            interceptors.add(new InterceptorConfig(resultSetImpl, "getObject", "(Ljava/lang/String;)Ljava/lang/Object;", MaskingObjectMethodVisitor.class));
        }
    }

    public void loadProperties() {
        configLoad = ContextConfigLoad.configLoad();
        keysPath = configLoad.getProperty("keysPath");
        logLevel = configLoad.getProperty("log.level");
        read = Boolean.parseBoolean(configLoad.getProperty("read"));
        driverScan = configLoad.getProperty("driver-scan");
        multiple = Boolean.parseBoolean(configLoad.getProperty("multiple"));
        refreshEnable = Boolean.parseBoolean(configLoad.getProperty("word.refresh.enable"));
        refreshTime = Integer.parseInt(configLoad.getProperty("word.refresh.interval"));
        users.addAll(StringUtil.split(configLoad.getProperty("include.users"), ","));
        dbs.addAll(StringUtil.split(configLoad.getProperty("include.dbs"), ","));
        tables.addAll(StringUtil.split(configLoad.getProperty("include.tables"), ","));
    }

    /**
     * 资源释放
     */
    @Override
    public void free() {
        for (RecyclableResource resource : resources) {
            resource.free();
        }
        ContextConfigLoad.clear();
        interceptors = null;
        maskingProcess = null;
        ServiceProvider.clear();
    }

    private String keysPath;
    private String logLevel;
    private boolean read;
    private String driverScan;
    private boolean multiple;
    private boolean refreshEnable;
    private int refreshTime;
    private List<String> users = new ArrayList<>();
    private List<String> dbs = new ArrayList<>();
    private List<String> tables = new ArrayList<>();

    public String getKeysPath() {
        return keysPath;
    }

    public boolean getRead() {
        return read;
    }

    public String getLogLevel() {
        return logLevel;
    }

    public Interceptors getInterceptors() {
        return interceptors;
    }

    public String getDriverScan() {
        return driverScan;
    }

    public boolean isMultiple() {
        return multiple;
    }

    public boolean isRefreshEnable() {
        return refreshEnable;
    }

    public int getRefreshTime() {
        return refreshTime;
    }

    public List<String> getUsers() {
        return users;
    }

    public List<String> getDbs() {
        return dbs;
    }

    public List<String> getTables() {
        return tables;
    }

    public DataMaskingProcess getMaskingProcess() {
        return maskingProcess;
    }

    private final long idDef = 1;
    private DataReductionCache CACHE_LIST;
    private AtomicLong id;
    public static final String REPLACEMENT_REGEX = "【敏([0-9a-zA-Z]+)】\\*\\*\\*";
    public static final String REPLACEMENT_STRING = "【敏%s】***";

    public DataReductionCache cache() {
        return CACHE_LIST;
    }

    /**
     * 添加要脱敏的字符串，返回脱敏后的key
     *
     * @param value
     * @return
     */
    public String addReduction(String value) {
        String cacheId = CACHE_LIST.key(value);
        if (cacheId == null) {
            long _id = id.getAndIncrement();
            if (id.get() < idDef) {
                synchronized (this) {
                    _id = id.getAndIncrement();
                    if (id.get() < idDef) {
                        id.set(idDef);
                        _id = idDef;
                    }
                }
            }
            cacheId = CACHE_LIST.addNode(Long.toHexString(_id), value);
        }
        String cacheKey = String.format(REPLACEMENT_STRING, cacheId);
        return cacheKey;
    }
}
